package hxbase.generic;

class CssSelector
{
	static public function tokenize(str:String)
	{
		return new CssSelector(str).selections;
	}
	
	var selections : Array<Array<SelectorToken>>;
	var blocks : Array<SelectorToken>;
	
	var char : Int;
	var prevChar : Int;
	var curr : SelectorToken;
	var filter : SelectorToken; // Attribute/Pseudo filter selector token
	// Maybe TODO ?
	// var prevFilter : SelectorToken = null; // Used to stack filters
	var text : String;
	
	var attSelector : Bool;
	var pseudoSelector : Bool;
	var inNotPseudo : Bool;
	var inQuotes : Bool;
	var inCombinator : Bool;
	
	private function new(str:String)
	{
		selections		= [];
		curr			= None;
		text			= '';
		attSelector		= false;
		pseudoSelector	= false;
		inQuotes		= false;
		inCombinator	= false;
		inNotPseudo		= false;
		
		for(x in 0 ... str.length)
		{
			prevChar = char;
			char = str.charCodeAt(x);
			
			// Handle strings in quotes (for attribute selectors)
			if(inQuotes)
			{
				if(char == 34 && prevChar != 92) // """ && != "\"
					inQuotes = false;
				else
					text += str.charAt(x);
				
				continue;
			}
			
			if(inCombinator) {
				if(isWhiteChar(char)) continue;
				closeWhitespace();
			}
			
			if(char == 44) {        // ","
				closeWhitespace();
				blocks = null;
				//curr = None;
				filter = null;
				attSelector = false;
				pseudoSelector = false;
				inQuotes = false;
			}
			
			if(!inCombinator) switch(char)
			{
				case 44:
					continue;
				case 9,10,13,32:	// \t,\n,\r," "
					if(pseudoSelector)
						closePseudo();
					else {
						// Ignore multiple whitespace chars
						if(isWhiteChar(prevChar)) continue;
						inCombinator = true;
						continue;
					}
				
				case 42:			// "*"
					if(!attSelector)
						curr = Any(None);
					else
						filter = AttContains(text, null);
				
				case 91:			// "["
					if(curr == None)
						curr = Element(text, None);
					else
						throw 'Parse error';
					
					attSelector = true;
				
				case 93:			// "]"
					if(filter == null) filter = AttExists(text);
					else filter = switch(filter)
					{
						case AttEquals(n, v):		AttEquals(n, text);
						case AttHasListed(n, v):	AttHasListed(n, text);
						case AttStartsWith(n, v):	AttStartsWith(n, text);
						case AttEndsWith(n, v):		AttEndsWith(n, text);
						case AttContains(n, v):		AttContains(n, text);
						case AttStartsWith_OptionallyFollowedByHyphen(n, v):
							 AttStartsWith_OptionallyFollowedByHyphen(n, text);
						default:
							throw 'Parse error'; 	null;
					}
					setFilter();
					attSelector = false;
				
				case 34:			// """
					inQuotes = true;
				
				case 61:			// "="
					if(!attSelector || curr == null) throw 'Parse error';
					if(filter == null)
						filter = AttEquals(text, null);
				
				case 126:			// "~"
					if(!attSelector) throw 'Parse error';
					filter = AttHasListed(text, null);
				
				case 94:			// "^"
					if(!attSelector) throw 'Parse error';
					filter = AttEndsWith(text, null);
				
				case 36:			// "$"
					if(!attSelector) throw 'Parse error';
					filter = AttStartsWith(text, null);
				
				case 124:			// "|"
					if(!attSelector) throw 'Parse error';
					filter = AttStartsWith_OptionallyFollowedByHyphen(text, null);
				
				case 58:			// ":"
					if(pseudoSelector) continue;
					
					if(curr == None)
						curr = Element(text, None);
					else switch(curr) {
						case Element(t, s):
							switch(s) {
								default:			throw 'Parse error: '+curr;	
								case Class(n,f):	curr = Element(t, Class(text,f));
								case ID(n,f):		curr = Element(t, ID(text,f));
							}
						default:
							throw 'Parse error: '+curr;
					}
					pseudoSelector = true;
				
				case 40:			// "("
					if(!pseudoSelector || attSelector) throw 'Parse error';
					openPseudo();
				
				case 41:			// ")"
					closePseudo();
				
				case 35, 46:		// "#", "."
					var newNode = (char == 46) ? SelectorToken.Class : SelectorToken.ID;
					
					if(text.length > 0 && curr == None)
						curr = Element(text, newNode(null, curr));
					else {
						switch(curr) {
							default:
								curr = newNode(text, curr);
							case Class(n,s):
								if(char == 46) throw 'Parse error'; // .
								trace(curr);
								curr = newNode(null, Class(text,s));
							case ID(n,s):
								if(char == 35) throw 'Parse error';
								curr = newNode(null, ID(text,s));
						}
					}
				
				default:
					text += str.charAt(x);
					continue; // Prevents text erase
			}
			text = '';
		}
		// Close the last block
		closeWhitespace();
	}
	
	function isWhiteChar(c:Int)
	{
		return switch(c){
			default: false;
			case 9,10,13,32: true; // \t,\n,\r," "
		};
	}
	
	function setFilter()
	{
		curr = switch(curr) {
			case Any(s):
				switch(s) {
					default: throw 'Parse error'; null;
					case None:				Any(filter);
					case Class(name, ss):	Any(Class(name, filter));
					case ID(name, ss):		Any(ID(name, filter));
				}
			case Element(t, s):
				switch(s) {
					default: throw 'Parse error'; null;
					case None:				Element(t, filter);
					case Class(name, ss):	Element(t, Class(name, filter));
					case ID(name, ss):		Element(t, ID(name, filter));
				}
			
			case Class(n, s):
				Class(text, s);
			
			case ID(n, s):
				ID(text, s);
			
			default:
				throw filter+' Parse error: '+curr; null;
		}
		
		if(this.inNotPseudo) switch(filter) {
			case Not(s):
				switch(s) {
					case Element(t, s):
						curr = Element(t, Not(curr));
						this.inNotPseudo = false;
					default:
						throw 'Parse error: '+curr; null;
				}
			default:
		}
		
		filter = null;
	}
	
	function openPseudo()
	{
		filter = switch(text) {
			case 'nth-child':			NthChild(0);
			case 'nth-last-child':		NthLastChild_CountFromLast(0);
			case 'nth-of-type':			NthOfType(0);
			case 'nth-last-of-type':	NthOfType_CountFromLast(0);
			case 'lang':				Language(null);
			case 'not':					inNotPseudo = true; pseudoSelector = false; Not(curr);
			default:					null;
		}
		if(inNotPseudo) curr = None;
	}
	
	function closePseudo(?skipSetFilter:Bool)
	{
		if(filter == null)
		{
			if(text.length <= 8) filter = switch(text) {
				case 'root':			DocumentRoot;
				case 'empty':			Empty;
				case 'link':			Link;
				case 'visited':			isVisitedLink;
				case 'active':			isActive;
				case 'hover':			isHovered;
				case 'focus':			hasFocus;
				case 'target':			linkTarget;
				case 'enabled':			Enabled;
				case 'disabled':		Disabled;
				case 'checked':			Checked;
				case 'before':			Before;
				case 'after':			After;
				default:				if(!inNotPseudo) throw curr+' Unrecognized pseudo selector: '+text; null;
			}
			else filter = switch(text) {
				case 'first-child':		FirstChild;
				case 'last-child':		LastChild;
				case 'first-of-type':	FirstSiblingOfType;
				case 'last-of-type':	LastSiblingOfType;
				case 'only-child':		OnlyChild;
				case 'only-of-type':	OnlySiblingOfType;
				case 'first-line':		FirstLine;
				case 'first-letter':	FirstLetter;
				case 'selection':		Selection;
				default:				if(!inNotPseudo) throw ' Unrecognized pseudo selector: '+text; null;
			}
		}
		else filter = switch(filter) {
			default: throw 'Parse error';
			case NthChild(n):					NthChild(Std.parseInt(text));
			case NthLastChild_CountFromLast(n):	NthLastChild_CountFromLast(Std.parseInt(text));
			case NthOfType(n):					NthOfType(Std.parseInt(text));
			case NthOfType_CountFromLast(n):	NthOfType_CountFromLast(Std.parseInt(text));
			case Language(t):					Language(text);
			case Not(f):						Not(f);
		}
		if(!skipSetFilter)
			setFilter();
		pseudoSelector = false;
	}
	
	function closeWhitespace()
	{
		if(pseudoSelector)
			closePseudo();
		
		switch(char)
		{
			case 62:        // ">"
				blocks.push(ChildOf(curr));
			case 43:        // "+"
				blocks.push(ImmediatelyPrecededBy(curr));
			case 126:       // "~"
				blocks.push(PrecededBy(curr));
			
			default:
			// New word
			if(curr == None && text.length > 0)
				curr = Element(text, curr);
			else switch(curr) {
				default:
				case Element(n, s):
					switch(s) {
						default:
						case Class(n,s):	if(n == null) curr = Element(n, Class(text, s));
						case ID(n,s):		if(n == null) curr = Element(n, ID(text, s));
					};
				case Class(n,s):	curr = Class(text, s);
				case ID(n,s):		curr = ID(text, s);
			}
			
			if(curr != None) {
				if(blocks == null)
					selections.push( blocks = [curr] );
				else
					blocks.push(DecendantOf(curr));
			}
			
			curr = None;
			inCombinator = false;
			text = '';
		}
	}
}

enum SelectorToken
{
	None;

	// Combinators
	/** 	E F, Filter on having E somwhere in the parent[.parent.*] chain **/	
	DecendantOf(selector:SelectorToken);
	/** 	E > F, Filter onter having E in parent **/
	ChildOf(selector:SelectorToken);
	/** 	E + F	**/
	ImmediatelyPrecededBy(selector:SelectorToken);
	/** 	E ~ F	**/
	PrecededBy(selector:SelectorToken);

	// Actual Selectors
	/** 	E.warning	**/
	Class(name:String, selector:SelectorToken);
	/** 	E#myid	**/
	ID(name:String, selector:SelectorToken);
	/** 	E:not(s)	**/
	Not(selector:SelectorToken);
	/** 	*	**/
	Any(selector:SelectorToken);
	/** 	E	**/
	Element(type:String, selector:SelectorToken);
	/** 	E[foo]	**/
	AttExists(name:String);
	/** 	E[foo="bar"]	**/
	AttEquals(name:String, value:String);
	/** 	E[foo~="bar"]	**/
	AttHasListed(name:String, value:String);
	/** 	E[foo^="bar"]	**/	
	AttStartsWith(name:String, value:String);
	/** 	E[foo$="bar"]	**/
	AttEndsWith(name:String, value:String);
	/** 	E[foo*="bar"]	**/
	AttContains(name:String, value:String);
	/** 	E[hreflang|="en"] or E[hreflang|="en-US"]
	 * 		an E element whose "hreflang" attribute has a hyphen-separated list of values beginning (from the left) with "en"
	 **/
	AttStartsWith_OptionallyFollowedByHyphen(name:String, value:String); // Rename please :-S
	/** 	E:root	**/
	DocumentRoot;
	/** 	E:nth-child(n)	**/
	NthChild(n:Int);
	/** 	E:nth-last-child(n)	**/
	NthLastChild_CountFromLast(n:Int);
	/** 	E:nth-of-type(n)	**/
	NthOfType(n:Int);
	/** 	E:nth-last-of-type(n)	**/
	NthOfType_CountFromLast(n:Int);
	/** 	E:first-child	**/
	FirstChild;
	/** 	E:last-child	**/
	LastChild;
	/** 	E:first-of-type	**/
	FirstSiblingOfType;
	/** 	E:last-of-type	**/
	LastSiblingOfType;
	/** 	E:only-child	**/
	OnlyChild;
	/** 	E:only-of-type	**/
	OnlySiblingOfType;
	/** 	E:empty	**/
	Empty;
	/** 	E:link	**/
	Link;
	/** 	E:visited	**/
	isVisitedLink;
	/** 	E:active	**/
	isActive;
	/** 	E:hover	**/
	isHovered;
	/** 	E:focus	**/
	hasFocus;
	/** 	E:target	**/
	linkTarget;
	/** 	E:lang(fr)	**/
	Language(name:String);
	/** 	E:enabled	**/
	Enabled;
	/** 	E:disabled	**/
	Disabled;
	/** 	E:checked	**/
	Checked;
	/** 	E::first-line	**/
	FirstLine;
	/** 	E::first-letter	**/
	FirstLetter;
	/** 	E::selection	**/
	Selection;
	/** 	E::before	**/
	Before;
	/** 	E::after	**/
	After;
}
